package com.nurflugel.picturebrowserservlet.gui;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URL;
import java.util.*;
import java.util.List;

/**
 * Class GifDecoder - Decodes a GIF file into one or more frames.<br>
 *
 * <pre>
   Example:
      GifDecoder d = new GifDecoder();
      d.read("sample.gif");
      int n = d.getFrameCount();
      for (int i = 0; i < n; i++) {
         BufferedImage frame = d.getFrame(i);  // frame i
         int t = d.getDelay(i);  // display duration of frame in milliseconds
         // do something with frame
      }
 * </pre>
 *
 * <p>No copyright asserted on the source code of this class. May be used for any purpose, however, refer to the Unisys LZW patent for any additional
 * restrictions. Please forward any corrections to kweiner@fmsware.com.</p>
 *
 * @author   Kevin Weiner, FM Software; LZW decoder adapted from John Cristy's ImageMagick.
 * @version  1.01 July 2001
 */
@SuppressWarnings("NumericCastThatLosesPrecision")
public class GifDecoder
{
  /** File read status: No errors. */
  public static final int STATUS_OK = 0;

  /** File read status: Error decoding file (may be partially decoded). */
  public static final int STATUS_FORMAT_ERROR = 1;

  /** File read status: Unable to open source. */
  public static final int     STATUS_OPEN_ERROR = 2;
  private static final int    MAX_STACK_SIZE    = 4096;           // max decoder pixel stack size
  private BufferedInputStream in;
  private int                 status;
  private int                 width;                              // full image width
  private int                 height;                             // full image height
  private boolean             gctFlag;                            // global color table used
  private int                 gctSize;                            // size of global color table
  private int                 loopCount         = 1;              // iterations; 0 = repeat forever
  private int[]               gct;                                // global color table
  private int[]               lct;                                // local color table
  private int[]               act;                                // active color table
  private int                 bgIndex;                            // background color index
  private int                 bgColor;                            // background color
  private int                 lastBgColor;                        // previous bg color
  private int                 pixelAspect;                        // pixel aspect ratio
  private boolean             lctFlag;                            // local color table flag
  private boolean             interlace;                          // interlace flag
  private int                 lctSize;                            // local color table size
  private int                 ix;                                 // current image rectangle
  private int                 iy;                                 // current image rectangle
  private int                 iw;                                 // current image rectangle
  private int                 ih;                                 // current image rectangle
  private Rectangle           lastRect;                           // last image rect
  private BufferedImage       image;                              // current frame
  private BufferedImage       lastImage;                          // previous frame
  private byte[]              block             = new byte[256];  // current data block
  private int                 blockSize         = 0;              // block size

  // last graphic control extension info
  private int     dispose      = 0;      // 0=no action; 1=leave in place; 2=restore to bg; 3=restore to prev
  private int     lastDispose  = 0;
  private boolean transparency = false;  // use transparent color
  private int     delay        = 0;      // delay in milliseconds
  private int     transIndex;            // transparent color index

  // LZW decoder working arrays
  private short[]        prefix;
  private byte[]         suffix;
  private byte[]         pixelStack;
  private byte[]         pixels;
  private List<GifFrame> frames;  // frames read from current file
  private int            frameCount;

  /** Creates a new GifDecoder object. */
  public GifDecoder() {}

  // ------------------------ OTHER METHODS ------------------------
  public BufferedImage getBufferedImage(BufferedInputStream bufferedImputStream)
  {
    read(bufferedImputStream);

    int           n     = frameCount;
    BufferedImage frame = null;

    if (n > 0)
    {
      frame = getFrame(0);
    }

    return frame;
  }

  /**
   * Reads GIF image from stream.
   *
   * @param   is  BufferedInputStream containing GIF file.
   *
   * @return  read status code (0 = no errors)
   */
  public int read(BufferedInputStream is)
  {
    init();

    if (is != null)
    {
      in = is;
      readHeader();

      if (!err())
      {
        readContents();

        if (frameCount < 0)
        {
          status = STATUS_FORMAT_ERROR;
        }
      }
    }
    else
    {
      status = STATUS_OPEN_ERROR;
    }

    try
    {
      is.close();
    }
    catch (IOException e) {}

    return status;
  }

  /** Initializes or re-initializes reader. */
  protected void init()
  {
    status     = STATUS_OK;
    frameCount = 0;
    frames     = new ArrayList<GifFrame>();
    gct        = null;
    lct        = null;
  }

  /** Reads GIF file header information. */
  protected void readHeader()
  {
    String id = "";

    for (int i = 0; i < 6; i++)
    {
      id += (char) read();
    }

    if (!id.startsWith("GIF"))
    {
      status = STATUS_FORMAT_ERROR;

      return;
    }

    readLSD();

    if (gctFlag && !err())
    {
      gct     = readColorTable(gctSize);
      bgColor = gct[bgIndex];
    }
  }

  /** Reads Logical Screen Descriptor. */
  protected void readLSD()
  {
    // logical screen size
    width  = readShort();
    height = readShort();

    // packed fields
    int packed = read();

    gctFlag     = (packed & 0x80) != 0;  // 1   : global color table flag
    gctSize     = 2 << (packed & 7);     // 6-8 : gct size
    bgIndex     = read();                // background color index
    pixelAspect = read();                // pixel aspect ratio
  }

  /**
   * Reads color table as 256 RGB integer values.
   *
   * @param   ncolors  int number of colors to read
   *
   * @return  int array containing 256 colors (packed ARGB with full alpha)
   */
  protected int[] readColorTable(int ncolors)
  {
    int    nbytes = 3 * ncolors;
    byte[] c      = new byte[nbytes];
    int    n      = 0;

    try
    {
      n = in.read(c);
    }
    catch (IOException e) {}

    int[] tab = null;

    if (n < nbytes)
    {
      status = STATUS_FORMAT_ERROR;
    }
    else
    {
      tab = new int[256];  // max size to avoid bounds checks

      int i = 0;
      int j = 0;

      while (i < ncolors)
      {
        int r    = c[j++] & 0xff;
        int g    = c[j++] & 0xff;
        int b    = c[j++] & 0xff;

        tab[i++] = 0xff000000 | (r << 16) | (g << 8) | b;
      }
    }

    return tab;
  }

  /** Returns true if an error was encountered during reading/decoding. */
  protected boolean err()
  {
    return status != STATUS_OK;
  }

  /** Main file parser. Reads GIF content blocks. */
  protected void readContents()
  {
    // read GIF file content blocks
    boolean done = false;

    while (!(done || err()))
    {
      int code = read();

      switch (code)
      {
        case 0x2C:       // image separator
          readImage();
          break;

        case 0x21:       // extension
          code = read();
          switch (code)
          {
            case 0xf9:   // graphics control extension
              readGraphicControlExt();
              break;

            case 0xff:   // application extension
              readBlock();

              StringBuilder app = new StringBuilder();

              for (int i = 0; i < 11; i++)
              {
                app.append((char) block[i]);
              }

              if (app.toString().equals("NETSCAPE2.0"))
              {
                readNetscapeExt();
              }
              else
              {
                skip();  // don't care
              }

              break;

            default:     // uninteresting extension
              skip();
          }              // end switch

          break;

        case 0x3b:       // terminator
          done = true;
          break;

        default:
          status = STATUS_FORMAT_ERROR;
      }                  // end switch
    }                    // end while
  }

  /** Reads next frame image. */
  protected void readImage()
  {
    ix = readShort();                  // (sub)image position & size
    iy = readShort();
    iw = readShort();
    ih = readShort();

    int packed = read();

    lctFlag   = (packed & 0x80) != 0;  // 1 - local color table flag
    interlace = (packed & 0x40) != 0;  // 2 - interlace flag
    lctSize   = 2 << (packed & 7);     // 6-8 - local color table size

    if (lctFlag)
    {
      lct = readColorTable(lctSize);   // read table
      act = lct;                       // make local table active
    }
    else
    {
      act = gct;                       // make global table active

      if (bgIndex == transIndex)
      {
        bgColor = 0;
      }
    }

    int save = 0;

    if (transparency)
    {
      save            = act[transIndex];
      act[transIndex] = 0;             // set transparent color if specified
    }

    if (act == null)
    {
      status = STATUS_FORMAT_ERROR;    // no color table defined
    }

    if (err())
    {
      return;
    }

    decodeImageData();                 // decode pixel data
    skip();

    if (err())
    {
      return;
    }

    frameCount++;

    // create new image to receive frame data
    image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE);
    setPixels();                             // transfer pixel data to image
    frames.add(new GifFrame(image, delay));  // add image to frame list

    if (transparency)
    {
      act[transIndex] = save;
    }

    resetFrame();
  }

  /** Decodes LZW image data into pixel array. Adapted from John Cristy's ImageMagick. */
  protected void decodeImageData()
  {
    int npix = iw * ih;

    if ((pixels == null) || (pixels.length < npix))
    {
      pixels = new byte[npix];  // allocate new pixel array
    }

    if (prefix == null)
    {
      prefix = new short[MAX_STACK_SIZE];
    }

    if (suffix == null)
    {
      suffix = new byte[MAX_STACK_SIZE];
    }

    if (pixelStack == null)
    {
      pixelStack = new byte[MAX_STACK_SIZE + 1];
    }

    // Initialize GIF data stream decoder.
    int data_size          = read();
    int clear              = 1 << data_size;
    int end_of_information = clear + 1;
    int available          = clear + 2;
    int NullCode           = -1;
    int old_code           = NullCode;
    int code_size          = data_size + 1;
    int code_mask          = (1 << code_size) - 1;
    int code;

    for (code = 0; code < clear; code++)
    {
      prefix[code] = 0;
      suffix[code] = (byte) code;
    }

    // Decode GIF pixel stream.
    int datum = 0;
    int bits  = 0;
    int count = 0;
    int first = 0;
    int top   = 0;
    int pi    = 0;
    int bi    = 0;
    int i;

    for (i = 0; i < npix;)
    {
      if (top == 0)
      {
        if (bits < code_size)  // Load bytes until there are enough bits for a code.
        {
          if (count == 0)      // Read a new data block.
          {
            count = readBlock();

            if (count <= 0)
            {
              break;
            }

            bi = 0;
          }

          datum += (block[bi] & 0xff) << bits;
          bits  += 8;
          bi++;
          count--;

          continue;
        }

        // Get the next code.
        code  =  datum & code_mask;
        datum >>= code_size;
        bits  -= code_size;

        // Interpret the code
        if ((code > available) || (code == end_of_information))
        {
          break;
        }

        if (code == clear)  // Reset decoder.
        {
          code_size = data_size + 1;
          code_mask = (1 << code_size) - 1;
          available = clear + 2;
          old_code  = NullCode;

          continue;
        }

        if (old_code == NullCode)
        {
          pixelStack[top++] = suffix[code];
          old_code          = code;
          first             = code;

          continue;
        }

        int in_code = code;

        if (code == available)
        {
          pixelStack[top++] = (byte) first;
          code              = old_code;
        }

        while (code > clear)
        {
          pixelStack[top++] = suffix[code];
          code              = prefix[code];
        }

        first = suffix[code] & 0xff;

        // Add a new string to the string table,
        if (available >= MAX_STACK_SIZE)
        {
          break;
        }

        pixelStack[top++] = (byte) first;
        prefix[available] = (short) old_code;
        suffix[available] = (byte) first;
        available++;

        if (((available & code_mask) == 0) && (available < MAX_STACK_SIZE))
        {
          code_size++;
          code_mask += available;
        }

        old_code = in_code;
      }  // end if

      // Pop a pixel off the pixel stack.
      top--;
      pixels[pi++] = pixelStack[top];
      i++;
    }                 // end for

    for (i = pi; i < npix; i++)
    {
      pixels[i] = 0;  // clear missing pixels
    }
  }

  /** Reads a single byte from the input stream. */
  protected int read()
  {
    int curByte = 0;

    try
    {
      curByte = in.read();
    }
    catch (IOException e)
    {
      status = STATUS_FORMAT_ERROR;
    }

    return curByte;
  }

  /**
   * Reads next variable length block from input.
   *
   * @return  number of bytes stored in "buffer"
   */
  protected int readBlock()
  {
    blockSize = read();

    int n = 0;

    if (blockSize > 0)
    {
      try
      {
        int count = 0;

        while (n < blockSize)
        {
          count = in.read(block, n, blockSize - n);

          if (count == -1)
          {
            break;
          }

          n += count;
        }
      }
      catch (IOException e) {}

      if (n < blockSize)
      {
        status = STATUS_FORMAT_ERROR;
      }
    }

    return n;
  }

  /** Creates new frame image from current data (and previous frames as specified by their disposition codes). */
  protected void setPixels()
  {
    // expose destination image's pixels as int array
    int[] dest = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();

    // fill in starting image contents based on last image's dispose code
    if (lastDispose > 0)
    {
      if (lastDispose == 3)                                  // use image before last
      {
        int n = frameCount - 2;

        if (n > 0)
        {
          lastImage = getFrame(n - 1);
        }
        else
        {
          lastImage = null;
        }
      }

      if (lastImage != null)
      {
        int[] prev = ((DataBufferInt) lastImage.getRaster().getDataBuffer()).getData();

        System.arraycopy(prev, 0, dest, 0, width * height);  // copy pixels

        if (lastDispose == 2)                                // fill last image rect area with background color
        {
          Graphics2D g = image.createGraphics();
          Color      c = null;

          if (transparency)
          {
            c = new Color(0, 0, 0, 0);                       // assume background is transparent
          }
          else
          {
            c = new Color(lastBgColor);                      // use given background color
          }

          g.setColor(c);
          g.setComposite(AlphaComposite.Src);                // replace area
          g.fill(lastRect);
          g.dispose();
        }
      }
    }                                                        // end if

    // copy each source line to the appropriate place in the destination
    int pass  = 1;
    int inc   = 8;
    int iline = 0;

    for (int i = 0; i < ih; i++)
    {
      int line = i;

      if (interlace)
      {
        if (iline >= ih)
        {
          pass++;

          switch (pass)
          {
            case 2:
              iline = 4;
              break;

            case 3:
              iline = 2;
              inc   = 4;
              break;

            case 4:
              iline = 1;
              inc   = 2;
          }
        }

        line        = iline;
        iline       += inc;
      }                      // end if

      line += iy;

      if (line < height)
      {
        int k    = line * width;
        int dx   = k + ix;   // start of line in dest
        int dlim = dx + iw;  // end of dest line

        if ((k + width) < dlim)
        {
          dlim = k + width;  // past dest edge
        }

        int sx = i * iw;     // start of line in source

        while (dx < dlim)    // map color and insert in destination
        {
          int index = pixels[sx++] & 0xff;
          int c     = act[index];

          if (c != 0)
          {
            dest[dx] = c;
          }

          dx++;
        }
      }
    }                        // end for
  }

  /** Resets frame state for reading next image. */
  protected void resetFrame()
  {
    lastDispose = dispose;
    lastRect    = new Rectangle(ix, iy, iw, ih);
    lastImage   = image;
    lastBgColor = bgColor;

    // int     dispose = 0;
    // boolean transparency = false;
    // int     delay        = 0;
    lct = null;
  }

  /** Reads Graphics Control Extension values. */
  protected void readGraphicControlExt()
  {
    read();                           // block size

    int packed = read();              // packed fields

    dispose = (packed & 0x1c) >> 2;   // disposal method

    if (dispose == 0)
    {
      dispose = 1;                    // elect to keep old image if discretionary
    }

    transparency = (packed & 1) != 0;
    delay        = readShort() * 10;  // delay in milliseconds
    transIndex   = read();            // transparent color index
    read();                           // block terminator
  }

  /** Reads next 16-bit value, LSB first. */
  protected int readShort()
  {
    // read 16-bit value, LSB first
    return read() | (read() << 8);
  }

  /** Reads Netscape extenstion to obtain iteration count. */
  protected void readNetscapeExt()
  {
    do
    {
      readBlock();

      if (block[0] == 1)  // loop count sub-block
      {
        int b1    = block[1] & 0xff;
        int b2    = block[2] & 0xff;

        loopCount = (b2 << 8) | b1;
      }
    }
    while ((blockSize > 0) && !err());
  }

  /** Skips variable length blocks up to and including next zero length block. */
  protected void skip()
  {
    do
    {
      readBlock();
    }
    while ((blockSize > 0) && !err());
  }

  /**
   * Gets the image contents of frame n.
   *
   * @return  BufferedImage representation of frame, or null if n is invalid.
   */
  public BufferedImage getFrame(int n)
  {
    BufferedImage im = null;

    if ((n >= 0) && (n < frameCount))
    {
      im = frames.get(n).image;
    }

    return im;
  }

  /**
   * Gets display duration for specified frame.
   *
   * @param   n  int index of frame
   *
   * @return  delay in milliseconds
   */
  public int getDelay(int n)
  {
    //
    delay = -1;

    if ((n >= 0) && (n < frameCount))
    {
      delay = frames.get(n).delay;
    }

    return delay;
  }

  /**
   * Gets the first (or only) image read.
   *
   * @return  BufferedImage containing first frame, or null if none.
   */
  public BufferedImage getImage()
  {
    return getFrame(0);
  }

  /**
   * Reads GIF file from specified source (file or URL string).
   *
   * @param   name  String containing source
   *
   * @return  read status code (0 = no errors)
   */
  public int read(String name)
  {
    status = STATUS_OK;

    try
    {
      String newName = name.trim();

      if (newName.indexOf("://") > 0)
      {
        URL url = new URL(newName);

        in = new BufferedInputStream(url.openStream());
      }
      else
      {
        in = new BufferedInputStream(new FileInputStream(name));
      }

      status = read(in);
    }
    catch (IOException e)
    {
      status = STATUS_OPEN_ERROR;
    }

    return status;
  }
  // ------------------------ GETTER/SETTER METHODS ------------------------

  /**
   * Gets the number of frames read from file.
   *
   * @return  frame count
   */
  public int getFrameCount()
  {
    return frameCount;
  }

  /**
   * Gets the "Netscape" iteration count, if any. A count of 0 means repeat indefinitiely.
   *
   * @return  iteration count if one was specified, else 1.
   */
  public int getLoopCount()
  {
    return loopCount;
  }

  private static class GifFrame
  {
    BufferedImage image;
    int           delay;

    GifFrame(BufferedImage im, int del)
    {
      image = im;
      delay = del;
    }
  }
}
